Tester.php

<?php

namespace Taeluf;

class Tester {

    protected $benches;
    protected $catchers = [];
    protected $disabled = false;

    protected $results = [];

    public function __construct(){
        $this->prepare();
        set_error_handler([$this,'throwError']);
    }
    public function throwError($errno, $errstr, $errfile, $errline) {
        throw new \ErrorException($errstr, $errno, 0, $errfile, $errline);
    }

    public function catch($exceptionClass,$strict=false){
        $catcher = new Tester\ExceptionCatcher($exceptionClass);
        $this->catchers[] = $catcher;
        return $catcher;
    }
    public function throw($e){
        $list = $this->catchers;
        foreach ($list as $index => $cat){
            if ($cat->matches($e)){
                //@TODO allow one catcher to be re-used & just require that each exception be caught by at least one.
                unset($this->catchers[$index]);
                return true;
            }
        }
        throw $e;
    }

    public function disable(){
        $this->disabled = true;
    }

    /**
     * Call `$tester->test('TestName')->compare($target,$actual)` to run a sub-test inside a test in your class
     * Chaining is not required
     * 
     * @export(Usage.test)
     */
    protected function test($subTestName){
        echo "****$subTestName****\n";
        return $this;
    }
    protected function startTest($key){
        echo "\n    ".'inner test \''.$key.'\' starts:';
    }
    protected function endTest($key,$result){
        echo "\n        ".'inner test \''.$key.'\' ends with result: <b>'.($result ? 'true' : 'false').'</b>';
    }

    /**
     * Get an array of method names on $this that start with 'test'
     */
    protected function getTestMethods(){
        $methods = get_class_methods($this);
        $methods = array_map(function($key,$value) //use ($builtInMethods)
        {
            if ($value=='test')return null;
            if (substr($value,0,4)!='test')return null;
            return $value;
        },array_keys($methods),array_values($methods));

        return $methods;
    }

    protected function startOb(){
        return Tester\Utility::startOb();
    }
    protected function endOb($ob_level){
        return Tester\Utility::endOb($ob_level);
    }


    /**
     * Run tests
     *
     * @param $methods an array of method names to run as tests or NULL to run all methods beginning with 'test'
     */
    public function run(?array $methods = null){
        if ($methods===null)$methods = $this->getTestMethods();
        
        // $this->results...
        foreach ($methods as $method){
            $result = ['method'=>substr($method,4), 'error'=>null];
            if ($method==NULL)continue;
            $this->catchers = [];
            $error = null;
            $retValue = null;
            $this->benchStart('test_'.$method);
            $ob_level = $this->startOb();
            try {
                $result['returnVal'] = $this->$method();
            } catch (\Throwable $t){
                $result['returnVal'] = null;
                $result['error'] = $t;
            } 
            $result['output'] = $this->endOb($ob_level);
            $result['bench'] = $this->benchEnd('test_'.$method);
            $result['result'] = $result['returnVal'] ? true : false;

            $result['disabled'] = $this->disabled;
            $this->disabled = false;
            if (($c=count($this->catchers))>0){
                // $result = false;
                $result['output'] = "---exception-fail---\n{$c} exceptions were not handled.\n----------\n".$result['output'];
            }
            $result['html'] = $this->htmlOutput((object)$result);
            $this->results[$method] = $result;
        }
    
        return $this->results;
        //now do what we do with results.
    }
    public function benchStart($key){
        $this->benches[$key]['start'] = microtime(true);
    }
    public function benchEnd($key){
        $end = microtime(true);
        $start = $this->benches[$key]['start'];
        unset($this->benches[$key]);
        $diff = $end - $start;
        return (object)[
            'start'=>$start,
            'end'=>$end,
            'diff'=>$diff
        ];
    }
    public function compare($target, $actual,$strict=false){
        if (!$strict&&is_string($target)&&substr($target,0,7)=='file://'){
            $file = substr($target,7);
            if (!is_file($file)){
                throw new \Exception("{$target} is not a file. ");
            }
            $ext = substr($file,-4);
            if ($ext=='.php'){
                ob_start();
                require($file);
                $target=ob_get_clean();
            }
            else $target=file_get_contents($file);
        }
        if (!$strict&&is_string($actual)&&substr($actual,0,7)=='file://'){
            $file = substr($actual,7);
            if (!is_file($file)){
                throw new \Exception("{$actual} is not a file. ");
            }
            $ext = substr($file,-4);
            if ($ext=='.php'){
                ob_start();
                require($file);
                $actual=ob_get_clean();
            }
            else $actual=file_get_contents($file);
        }


        if (!$strict&&is_string($target)&&is_string($actual)){
            $target = trim($target);
            $actual = trim($actual);
        }
        $pass = false;
        if ($strict&&$target===$actual)$pass = true;
        else if ($target==$actual)$pass = true;


        $target = $this->comparisonOutput($target);
        $actual = $this->comparisonOutput($actual);
        
        // echo "Strict: ".($strict ? 'true' : 'false');
        if ($pass)echo "+++pass+++";
        else echo "---fail---";
        if ($strict)echo " strict comparison";
        echo "\n";
        echo "Target:\n{$target}";
        echo "\n--\n";
        echo "Actual:\n{$actual}";
        echo "\n--------\n";

        return $pass;
    }

    public function comparisonOutput($value){
        if (is_object($value)){
            return "Object of class ".get_class($value);
        }
        if (is_array($value)){
            $pr = var_export($value,true);
            $oneLine = implode(" ",array_map('trim',explode("\n",$pr)));
            $value = substr($oneLine,0,200);
            if (strlen($oneLine)>200)$value .= '...';

            return $value;
        }

        if ($value===true)return 'true';
        else if ($value===false)return 'false';

        return $value;
    }

    //@export_start(Example.ModifyOutput)
    public function htmlOutput($details){
        ob_start();

        $successStatement = $details->result ? '<span style="color:green;">success</span>' : '<span style="color:red;">fail</span>';
        if ($details->error!=null)$successStatement = '<strong style="color:blue;">error</strong>';
        if ($details->disabled===true)$successStatement = '<strong style="color:orange;">disabled</strong>';
        $diff = $details->bench->diff;
        if ($diff < 0.0001)$diff = '';
        else $diff = 'in '.number_format($diff*1000,3).'ms';
        echo "<details>\n    <summary><b>".$details->method.":</b> ".$successStatement." {$diff}   </summary>\n";
            // echo "    <div>Time to run: ".$details->bench->diff."</div>";
            echo "    <div style='padding-left:4ch;white-space:pre;'>\n";
                $detailsOutput = htmlentities($details->output);
                $detailsLines = explode("\n",$detailsOutput);
                
                $detailsLines = array_map(function($value){return '        '.$value;},$detailsLines);
                echo implode("\n",$detailsLines);
                // var_dump($detailsLines);
            echo "\n    </div>";
            if ($details->error!=null){
                echo "\n    <br>\n";
                echo "    <div style='color:red;padding-left:4ch;white-space:pre;'>\n";
                    $errorOutput = $details->error;
                    $errorLines = explode("\n",$errorOutput);
                    $errorLines = array_map(function($value){return '        '.$value;},$errorLines);
                    echo implode("\n",$errorLines);
                echo "\n    </div>";
            }
        echo "\n</details>\n";

        return ob_get_clean();
    }
    //@export_end(Example.ModifyOutput)

    public function prepare(){

    }


    /**
     * On a class that extends `\Taeluf\Tester`, call `ExtendingClass::runAll()` to run the tests.
     * 
     * @deprecated this function no longer does anything.
     * @export(RunTests.All) 
     */
    static public function runAll(){
        // $tester = new static();
        // $tester->run();
        // return $tester;
    }

    /**
     * 
     * @deprecated this function no longer does anything
     */
    static public function runTests($name){
        // $tester = new static();
        // $tester->run($name);
        // return $tester;
    }

    /**
     * 
     * @deprecated this function no longer does anything
     */
    static public function runAllToFile($filePath,$andPrint = true){
        // ob_start();
        // $tester = static::runAll();
        // $output = ob_get_clean();
        // if ($andPrint)echo $output;
        // file_put_contents($filePath, $output);
//
        // return $tester;
    }
    /**
     * Call `Taeluf\Tester::xdotoolRefreshFirefox($switchBackToCurrWindow = false)` to refresh your browser tab.
     * If you're writing you're using `runAllToFile($file)`, this could come in handy.
     * 
     * @deprecated in favor of \Taeluf\Tester\Utility::xdotoolRefreshFirefox()
     * @export(Extra.RefreshBrowserTab)
     */
    static public function xdotoolRefreshFirefox($switchBackToWindow = false){
        $args = $switchBackToWindow ? ' y' : '';
        system(__DIR__.'/reload.sh'.$args);

        \Taeluf\Tester\Utility::xdotoolRefreshFirefox($switchBackToWindow);
    }

}